#include "webbridge.h"
#include "bridgeobjectmanager.h"
#include "libs/base/utils/utils.h"
#include "webinterface.h"
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMetaMethod>
#include <QtDebug>
#include <QWebEnginePage>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWidget>
#include <utility>

WebBridge::WebBridge(QWidget* view, QWebEnginePage* page, WebInterface* web_interface,
                     BridgeObjectManager* object_mgr, QObject* parent)
    : QObject(parent),
      view_(view),
      page_(page),
      web_interface_(web_interface),
      object_mgr_(object_mgr),
      bridge_object_id_(BridgeObjectManager::bridgeObjectId())
{
    BridgeObject bo;
    bo.setCallbackFilter(BridgeCallbackFilter(this, &WebBridge::invokeJsCallback));
    object_mgr->registerObject(bridge_object_id_, bo);
}

WebBridge::~WebBridge() { }

bool WebBridge::inject(const QString& object_name, const QString& script_name,
                       const QString& prefix) const
{
    if (page_) {
        const QList<QString> names { "invokeMethod", "invokeCallback", "notifyEvent" };
        const QList<QMetaMethod> methods = metaMethods(this->metaObject(), names);
        if (!methods.empty()) {
            QString source_code;
            for (const QMetaMethod& m : std::as_const(methods)) {
                source_code += scriptSourceCode(m, object_name, prefix);
                source_code += '\n';
            }
            return injectScript(*page_, source_code, script_name);
        }
    }
    return false;
}

bool WebBridge::invokeJsCallback(const QString& callback, const QString& args, bool keep)
{
    if (web_interface_ && object_mgr_) {
        return web_interface_->invokeJsCallback(object_mgr_->frameworkName(), callback, args, keep);
    }
    return false;
}

void WebBridge::notifyClientEvent(const QString& object_id, const QString& event_name,
                                  const QString& args)
{
    if (web_interface_) {
        const QString js_event_name = BridgeObjectManager::uniformObjectId(object_id, event_name);
        web_interface_->notifyClientEvent(js_event_name, args);
    }
}

void WebBridge::invokeJsMethod(const QString& method, const QString& args,
                               const QString& success_callback, const QString& failure_callback,
                               const QString& sender_id)
{
    if (web_interface_ && object_mgr_) {
        web_interface_->invokeJsMethod(object_mgr_->frameworkName(), method, args, success_callback,
                                       failure_callback, sender_id);
    }
}

bool WebBridge::injectWebChannelScript(QWebEnginePage& page)
{
    QFile file(":/qtwebchannel/qwebchannel.js");
    if (file.open(QFile::ReadOnly)) {
        const QByteArray ba = file.readAll();
        file.close();
        const QString code = QString::fromLatin1(ba);
        return injectScript(page, code, genScriptName());
    }
    return false;
}

bool WebBridge::injectObject(QWebEnginePage& page, QObject* bridge, const QString& object_name,
                             const QList<QString>& method_names, const QString& script_name,
                             const QString& prefix)
{
    Q_ASSERT(bridge);
    Q_ASSERT(!object_name.isEmpty());
    const QList<QMetaMethod> methods = metaMethods(bridge->metaObject(), method_names);
    if (!methods.empty()) {
        QString source_code;
        for (const QMetaMethod& m : methods) {
            source_code += scriptSourceCode(m, object_name, prefix);
            source_code += '\n';
        }
        return injectScript(page, source_code, script_name);
    }
    return false;
}

bool WebBridge::injectScript(QWebEnginePage& page, const QString& source_code,
                             const QString& script_name, quint32 world_id,
                             QWebEngineScript::InjectionPoint point)
{
    QWebEngineScriptCollection& sl = page.scripts();
    if (sl.find(script_name).empty()) {
        QWebEngineScript s;
        s.setName(script_name);
        s.setSourceCode(source_code);
        s.setInjectionPoint(point);
        s.setWorldId(world_id);
        s.setRunsOnSubFrames(true);
        sl.insert(s);
        return true;
    }
    return false;
}

QString WebBridge::scriptSourceCode(const QMetaMethod& method, const QString& object_name,
                                    const QString& prefix)
{
    QString args;
    QString args_stats;
    const QList<QByteArray> names = method.parameterNames();
    for (const QByteArray& s : names) {
        const QString t(s);
        args += s;
        args += ',';
        args_stats += QString("if (typeof (%1) === 'undefined') {%1 = '';}\n").arg(t);
    }
    args.chop(1);

    const QString method_index = QString::number(method.methodIndex());
    const QString method_name = QString::fromLatin1(method.name());
    const QString func_name(prefix + method_name);

    // qmetaobjectpublisher_p.h
    //    const QString KEY_SIGNALS = QStringLiteral("signals");
    //    const QString KEY_METHODS = QStringLiteral("methods");
    //    const QString KEY_PROPERTIES = QStringLiteral("properties");
    //    const QString KEY_ENUMS = QStringLiteral("enums");
    //    const QString KEY_QOBJECT = QStringLiteral("__QObject*__");
    //    const QString KEY_ID = QStringLiteral("id");
    //    const QString KEY_DATA = QStringLiteral("data");
    //    const QString KEY_OBJECT = QStringLiteral("object");
    //    const QString KEY_DESTROYED = QStringLiteral("destroyed");
    //    const QString KEY_SIGNAL = QStringLiteral("signal");
    //    const QString KEY_TYPE = QStringLiteral("type");
    //    const QString KEY_METHOD = QStringLiteral("method");
    //    const QString KEY_ARGS = QStringLiteral("args");
    //    const QString KEY_PROPERTY = QStringLiteral("property");
    //    const QString KEY_VALUE = QStringLiteral("value");

    //    // NOTE: keep in sync with corresponding maps in qwebchannel.js and WebChannelTest.qml
    //    enum MessageType {
    //        TypeInvalid = 0,

    //        TYPES_FIRST_VALUE = 1,

    //        TypeSignal = 1,
    //        TypePropertyUpdate = 2,
    //        TypeInit = 3,
    //        TypeIdle = 4,
    //        TypeDebug = 5,
    //        TypeInvokeMethod = 6,
    //        TypeConnectToSignal = 7,
    //        TypeDisconnectFromSignal = 8,
    //        TypeSetProperty = 9,
    //        TypeResponse = 10,

    //        TYPES_LAST_VALUE = 10
    //    };
    return QString("function %1(%2){\ntry{\n%3"
                   "window.qt.webChannelTransport.send(JSON.stringify({type:6,object:'%4',id:'%4',"
                   "method:%5,args:[%2]}));\n"
                   "}catch(e){\nconsole.warn('invoke qt webchannel call failed', e);\n}}\n"
                   "window.%4 = window.%4 || {}; window.%4.%6 = %1;\n")
            .arg(func_name, args, args_stats, object_name, method_index, method_name);
}

QMetaMethod WebBridge::metaMethod(const QMetaObject* object, const QString& name)
{
    QMetaMethod method;
    Q_ASSERT(object);
    int count = object->methodCount();
    for (int i = 0; i < count; ++i) {
        QMetaMethod m = object->method(i);
        if (QString::fromLatin1(m.name()).compare(name) == 0) {
            method = m;
            break;
        }
    }
    return method;
}

QList<QMetaMethod> WebBridge::metaMethods(const QMetaObject* object, const QList<QString>& names)
{
    QList<QMetaMethod> methods;
    Q_ASSERT(object);
    for (const QString& s : names) {
        QMetaMethod m = metaMethod(object, s);
        if (m.isValid()) {
            methods.push_back(m);
        }
    }
    return methods;
}

QString WebBridge::createResponse(const QJsonValue& data, int status, const QString& message)
{
    const QJsonObject jo { { "data", data }, { "status", status }, { "message", message } };
    const QByteArray ba = QJsonDocument(jo).toJson(QJsonDocument::Compact);
    QString json = QString::fromUtf8(ba);
    return yc::baseutils::formatJavaScriptString(json);
}

void WebBridge::invokeJsMethod(QWebEnginePage& page, const QString& framework_name,
                               const QString& method, const QString& args,
                               const QString& success_callback, const QString& failure_callback,
                               const QString& id)
{
    const QString code =
            QString("if (typeof(%1.invokeFromClient) == 'function') { "
                    "%1.invokeFromClient('%2','%3','%4','%5','%6'); }")
                    .arg(framework_name, method, args, success_callback, failure_callback, id);
    page.runJavaScript(code);
}

QString WebBridge::genScriptName()
{
    static int id = 0;
    return QString("yc.wb.script_%1").arg(++id);
}

void WebBridge::invokeMethod(const QString& method_name, const QString& args,
                             const QString& success_callback, const QString& failure_callback,
                             const QString& sender_id)
{
    QString new_method_name;
    if (object_mgr_) {
        new_method_name = object_mgr_->methodMappingTable().value(method_name, method_name);
    } else {
        new_method_name = method_name;
    }
    const QString uniform_success_callback =
            BridgeObjectManager::uniformObjectId(bridge_object_id_, success_callback);
    const QString uniform_failure_callback =
            BridgeObjectManager::uniformObjectId(bridge_object_id_, failure_callback);
    QString ret;
    QWidget* sender_view = view_;
    bool ok = method_filter_(new_method_name, args, uniform_success_callback,
                             uniform_failure_callback, sender_id, sender_view, ret);
    if (ok) {
        // 来自WebPage的调用，直接回调
        if (page_ != nullptr && !success_callback.isEmpty() && !ret.isEmpty()) {
            invokeJsCallback(success_callback, ret);
        }
    } else {
        QString object_id;
        QString object_method_name;
        if (!BridgeObjectManager::resolveUniformObjectId(new_method_name, object_id,
                                                         object_method_name)) {
            if (object_mgr_) {
                object_id = object_mgr_->frameworkName();
            }
            object_method_name = new_method_name;
        }
        if (object_mgr_ && object_mgr_->contains(object_id)) {
            BridgeObject obj = object_mgr_->object(object_id);
            bool ok = obj.methodFilter()(object_method_name, args, uniform_success_callback,
                                         uniform_failure_callback, sender_id, sender_view, ret);
            if (!ok && obj.methods().contains(object_method_name)) {
                ret = obj.methods().value(object_method_name)(args, uniform_success_callback,
                                                              uniform_failure_callback, sender_id,
                                                              sender_view);
            }
            // 来自WebPage的调用，直接回调
            if (page_ != nullptr && !success_callback.isEmpty() && !ret.isEmpty()) {
                invokeJsCallback(success_callback, ret);
            }
        }
        Q_EMIT methodInvoked(object_id, object_method_name, args, uniform_success_callback,
                             uniform_failure_callback, sender_id, sender_view);
    }
}

void WebBridge::invokeCallback(const QString& callback, const QString& args)
{
    if (object_mgr_) {
        object_mgr_->invokeCallback(callback, args);
    }
    Q_EMIT callbackInvoked(callback, args);
}

void WebBridge::notifyEvent(const QString& event_type, const QString& args)
{
    QString object_id;
    QString object_event_type;
    if (!BridgeObjectManager::resolveUniformObjectId(event_type, object_id, object_event_type)) {
        object_id = object_mgr_->frameworkName();
        object_event_type = event_type;
    }
    if (object_mgr_ && object_mgr_->contains(object_id)) {
        BridgeObject obj = object_mgr_->object(object_id);
        if (obj.eventFilter().isValid()) {
            obj.eventFilter()(object_id, object_event_type, args);
        } else if (obj.events().contains(object_event_type)) {
            obj.events().value(object_event_type)(args);
        }
    }
    Q_EMIT eventNotified(object_id, object_event_type, args);
}
